今天應該會完成大致上的呈現~
// App.tsx
import React, { useState } from 'react';
import LineChart from './Component/LineChart';
import PricingControl from './Component/PricingControl';
import { SaleRecord } from './Component/types';
import { simulateSales } from './Component/salesLogic';
import SalesTable from './Component/SalesTable';
import SummaryTable from './Component/SummaryTable';
const INITIAL_STOCK = 2000;
const App: React.FC = () => {
const [salesData, setSalesData] = useState<SaleRecord[]>([
{ week: 1, price: 60, initialStock: INITIAL_STOCK, sales: 0, remainingStock: INITIAL_STOCK, revenue: 0, accumulatedRevenue: 0 },
]);
const handlePricingSelect = (price: number) => {
const currentWeek = salesData.length;
const previousRecord = salesData[currentWeek - 1];
const sales = simulateSales(previousRecord.remainingStock);
const newRemainingStock = previousRecord.remainingStock - sales;
const revenue = sales * price;
const accumulatedRevenue = previousRecord.accumulatedRevenue + revenue;
const record: SaleRecord = {
week: currentWeek + 1,
price,
initialStock: INITIAL_STOCK,
sales,
remainingStock: newRemainingStock,
revenue,
accumulatedRevenue,
};
setSalesData([...salesData, record]);
};
return (
<div className="App" style={{ textAlign: 'center' }}>
<h1 style={{ backgroundColor: 'green', color: 'white', padding: '20px', borderRadius: '5px' }}>
Retail Simulation
</h1>
<div style={{ width: "60%", margin: "auto" }}>
<LineChart data={salesData} />
</div>
<PricingControl onSelect={handlePricingSelect} isGameEnded={salesData.length >= 15} />
<SalesTable data={salesData} />
<SummaryTable salesData={salesData} />
</div>
);
};
export default App;
把 LineChart
的部分寬度稍微調小一點,避免完全看不到販售的資料。
並新增 SummaryTable
來處理回合結束後的統計資料。
// components/LineChart.tsx
import React from 'react';
import { Line } from 'react-chartjs-2';
import { SaleRecord } from './types';
import { Chart, registerables } from 'chart.js';
type Props = {
data: SaleRecord[];
};
const LineChart: React.FC<Props> = ({ data }) => {
Chart.register(...registerables);
const options = {
scales: {
y: {
min: 0,
max: 2000
},
x: {
ticks: {
autoSkip: false,
maxRotation: 0,
minRotation: 0
}
}
}
};
const chartData = {
labels: data.map(record => `Week ${record.week}`),
datasets: [
{
label: 'Remaining Stock',
data: data.map(record => record.remainingStock),
fill: false,
backgroundColor: 'rgb(75, 192, 192)',
borderColor: 'rgba(75, 192, 192, 0.2)',
},
],
};
return <Line data={chartData} options={options} />;
};
export default LineChart;
透過 options
:來調整Y軸最高最低值,以及一些小參數。
// types.tsx
export type SaleRecord = {
week: number;
price: number; // 價錢
initialStock: number; // 存貨,初始的存貨量
sales: number; // 銷售,賣出的數量
remainingStock: number; // 剩餘存貨 = 初始存貨 - 賣出的數量
revenue: number; // 營收 = 價錢 x 賣出的數量
accumulatedRevenue: number; // 累積營收
};
export type Summary = {
residualValue: number; // 殘值
totalRevenue: number; // 總營收
maxPossibleRevenue: number; // 最大可能營收
decisionQuality: number; // 決策品質(總營收/最大可能營收)
};
// Component/salesLogic.tsx
import { SaleRecord, Summary } from './types';
export const simulateSales = (remainingStock: number): number => {
const sales = Math.floor(Math.random() * (120 - 70 + 1) + 70);
return Math.min(sales, remainingStock);
}
export const calculateSummary = (salesData: SaleRecord[]): Summary => {
const finalWeekData = salesData[salesData.length - 1];
const MIN_PRICE = 36;
const residualValue = finalWeekData.remainingStock * MIN_PRICE;
const totalRevenue = salesData.reduce((sum, record) => sum + record.price * (record.initialStock - record.remainingStock), 0);
const maxPossibleRevenue = totalRevenue + residualValue;
const decisionQuality = totalRevenue / maxPossibleRevenue;
return {
residualValue,
totalRevenue,
maxPossibleRevenue,
decisionQuality
};
};
const residualValue
:計算剩餘存貨並用最低價格來計算。const totalRevenue
:總營收,就統計一下每週營收。const maxPossibleRevenue
:最大可能營收,就是加上殘值,假設能賣完的話。const decisionQuality
:本次決策品質,(總營收/最大可能營收)的百分比。import React, { useState, useEffect } from 'react';
import { SaleRecord, Summary } from './types';
import { calculateSummary } from './salesLogic';
type Props = {
salesData: SaleRecord[];
};
const SummaryTable: React.FC<Props> = ({ salesData }) => {
const [summary, setSummary] = useState<Summary | null>(null);
useEffect(() => {
if (salesData.length === 15) {
const gameSummary = calculateSummary(salesData);
setSummary(gameSummary);
}
}, [salesData]);
if (!summary) return null;
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
<div>
<table style={{ width: '100%', textAlign: 'center'}}>
<thead>
<tr>
<th style={{ padding: '0 100px' }}>殘值</th>
<th style={{ padding: '0 100px' }}>總營收</th>
<th style={{ padding: '0 100px' }}>最大可能營收</th>
</tr>
</thead>
<tbody>
<tr>
<td>${summary.residualValue.toLocaleString()}</td>
<td>${summary.totalRevenue.toLocaleString()}</td>
<td>${summary.maxPossibleRevenue.toLocaleString()}</td>
</tr>
</tbody>
</table>
</div>
<h2 style={{ fontWeight: 'bold', color: 'black', fontSize: '1.2em' }}>本次決策品質(總營收/最大可能營收)</h2>
<h1 style={{ fontWeight: 'bold', color: 'green', fontSize: '1.6em' }}>{(summary.decisionQuality * 100).toFixed(2)}%</h1>
</div>
);
};
export default SummaryTable;
一連串新知識,react 和前端知識(雖然以前有寫,但是都忘得差不多了)不過也終於完成這一個小專案。從一開始的概念發想,到逐步逐步的完成每一功能,每一步都充滿了記憶的回顧與探索新知識。對我而言,最大的收穫不僅僅是寫了一小專案,更是在過程中對於 React 和 TypeScript 的了解。接下來,將會繼續深入探索 TypeScript,希望能夠更加熟練地運用它。